# -*- coding: utf-8 -*-
"""
@Author: thekernel
@Date: 2020/5/29 10:22
@Description: 登陆 B站 破解滑动验证码
"""
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver import ActionChains
from selenium.common.exceptions import TimeoutException

import time
import base64
import random
from PIL import Image

USERNAME = '15658668665'  # 用户名
PASSWORD = 'Kernel990621'  # 密码
BORDER = 10  # 偏差距离
INIT_LEFT = 50  # 初始化滑块开始位置


def is_pixel_equal(image1, image2, x, y):
    """
    判断两张图片 各个位置的像素是否相同
    :param image1: 不带缺口的图片
    :param image2: 带缺口的图片
    :param x: 位置x
    :param y: 位置y
    :return: (x,y)位置的像素是否相同
    """
    # 获取两张图片指定位置的像素点
    pixel1 = image1.load()[x, y]
    pixel2 = image2.load()[x, y]
    # 设置一个阈值 允许有误差
    threshold = 60
    # 彩色图 每个位置的像素点有三个通道
    if abs(pixel1[0] - pixel2[0]) < threshold and abs(pixel1[1] - pixel2[1]) < threshold and abs(
            pixel1[2] - pixel2[2]) < threshold:
        return True
    else:
        return False


def get_gap(image1, image2):
    """
    获取缺口偏移量
    :param image1: 不带缺口的图片
    :param image2: 带缺口的图片
    :return: 缺口图片中应该移动的偏移量
    """
    left = INIT_LEFT
    for i in range(INIT_LEFT, image1.size[0]):
        for j in range(image1.size[1]):
            if not is_pixel_equal(image1, image2, i, j):
                left = i  # 找到缺口的左侧边界 在 x 方向上的位置
                return left
    return left


def get_track(gap):
    """
    根据偏移量 获取移动轨迹
    :param gap: 偏移量
    :return: 移动轨迹
    """
    track = []  # 移动轨迹
    current = 0  # 当前位移
    mid = gap * 4 / 5  # 前4/5段加速 后1/5段减速
    t = random.randint(2, 3) / 10  # 计算间隔
    v = 20  # 初速度

    while current < gap:
        if current < mid:
            a = 5  # 加速度为+5
        else:
            a = -5  # 加速度为-5

        v0 = v
        v = v0 + a * t
        move = v0 * t + 1 / 2 * a * t * t

        current += move
        track.append(round(move))

    return track


class BilibiliLoginSpider:
    def __init__(self):
        firefox_driver = "/Users/thekernel/Code/PythonWorkspace/PythonLib/geckodriver"
        self.url = 'https://passport.bilibili.com/login'  # B站登录界面
        self.browser = webdriver.Firefox(executable_path=firefox_driver)
        self.wait = WebDriverWait(self.browser, 20)  # 定义显示等待
        self.username = USERNAME
        self.password = PASSWORD

    def __del__(self):
        self.browser.close()  # 关闭浏览器

    def login_successfully(self):
        """
        判断是否登陆成功
        :return:
        """
        try:
            return bool(
                WebDriverWait(self.browser, 5).until(EC.presence_of_element_located((By.XPATH, '//a[@title="消息"]')))
            )
        except TimeoutException:
            return False

    def move_to_gap(self, slider, track):
        """
        拖动滑块到缺口处
        :param slider:滑块
        :param track: 轨迹
        :return:
        """
        ActionChains(self.browser).click_and_hold(slider).perform()
        for x in track:
            ActionChains(self.browser).move_by_offset(xoffset=x, yoffset=0).perform()
        time.sleep(0.5)
        ActionChains(self.browser).release().perform()  # 松开鼠标

    def get_slider(self):
        """
        获取滑块
        :return: 滑块对象
        """
        '''
        <div class="geetest_slider_button" style="opacity: 1; transform: translate(0px, 0px);"></div>
        '''
        slider = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.geetest_slider_button')))

        return slider

    def get_image(self):
        """
        获取验证码图片
        :return: 图片对象
        """
        '''
        <canvas class="geetest_canvas_bg geetest_absolute" height="160" width="260"></canvas>
        '''
        # 带阴影的图片
        im = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.geetest_canvas_bg')))
        time.sleep(2)
        im.screenshot('captcha.png')

        # 执行 JS 代码并拿到图片 base64 数据(不带阴影的完整图片)
        full_bg_js = 'return document.getElementsByClassName("geetest_canvas_fullbg")[0].toDataURL("image/png");'
        im_info = self.browser.execute_script(full_bg_js)  # 执行js文件得到带图片信息的图片数据
        # 拿到base64编码的图片信息
        im_base64 = im_info.split(',')[1]
        # 转为bytes类型
        captcha1 = base64.b64decode(im_base64)
        # 将图片保存在本地
        with open('captcha1.png', 'wb') as f:
            f.write(captcha1)

        bg_js = 'return document.getElementsByClassName("geetest_canvas_bg")[0].toDataURL("image/png");'  # 带阴影的图片
        im_info = self.browser.execute_script(bg_js)
        im_base64 = im_info.split(',')[1]
        captcha2 = base64.b64decode(im_base64)
        with open('captcha2.png', 'wb') as f:
            f.write(captcha2)

        captcha1 = Image.open('captcha1.png')
        captcha2 = Image.open('captcha2.png')
        return captcha1, captcha2

    def get_login_btn(self):
        """
        获取登陆按钮
        :return: None
        """
        '''
        <a class="btn btn-login">登录</a>
        '''
        button = self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, '.btn-login')))
        return button

    def open(self):
        """
        打开登陆界面，输入用户名和密码
        :return: None
        """
        self.browser.get(self.url)  # 打开网址
        '''
        <input type="text" value="" placeholder="你的手机号/邮箱" id="login-username" maxlength="50" autocomplete="off" class="">
        '''
        username = self.wait.until(EC.presence_of_element_located((By.ID, 'login-username')))
        '''
        <input type="password" placeholder="密码" id="login-passwd" class="">
        '''
        password = self.wait.until(EC.presence_of_element_located((By.ID, 'login-passwd')))

        username.send_keys(self.username)
        password.send_keys(self.password)

    def login(self):
        self.open()
        button = self.get_login_btn()  # 找到登录按钮
        button.click()  # 点击

        image1, image2 = self.get_image()

        gap = get_gap(image1, image2)
        gap -= BORDER

        track = get_track(gap)
        slider = self.get_slider()

        self.move_to_gap(slider, track)

        if self.login_successfully():
            print("登录成功")
        else:  # 可能不成功 再试一次
            time.sleep(5)
            self.login()


if __name__ == '__main__':
    bilibili = BilibiliLoginSpider()
    bilibili.login()
